// scalaxx - Scala XML eXtensions
// (c) 2009-2010, Normen Müller, normen.mueller@gmail.com
// $Id: Differ.scala 79 2010-05-13 12:35:20Z normen.mueller $
package scalaxx.diff

import java.io.File

import scala.xml.{NodeSeq, NodeBuffer, Node, Text, MetaData, Null}
import scala.xml.XML.{loadFile}
import scala.xml.Utility.{trimProper}

import scaup.Views._
import scaup.eq.Slit

import scalaxx.Views._
import scalaxx.path._
import scalaxx.path.LocationPath

/** Companion to <code>Differ</code> class.
 * 
 *  @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 *  @version 0.0.3
 */
object Differ {
  type E[M <: {def get(key: String): Option[Seq[T]]}, T] = scaup.eq.EquivalenceSystem[M, T]
  
  def diff(src: File, tgt: File): List[Command] = diff(new BaseEquivalenceSystem, src, tgt)
  
  def diff(src: NodeSeq, tgt: NodeSeq): List[Command] = diff(new BaseEquivalenceSystem, src, tgt)
  
  def diff(eqspec: File, src: File, tgt: File): List[Command] = diff(EquivalenceSystem.parse(eqspec), src, tgt)
  
  def diff(eqspec: File, src: NodeSeq, tgt: NodeSeq): List[Command] = diff(EquivalenceSystem.parse(eqspec), src, tgt)
  
  def diff[M <: MetaData, T >: Node](eqsys: E[M, T], src: File, tgt: File): List[Command] = 
    try {
      diff(eqsys, loadFile(src), loadFile(tgt))
    } catch {
      case e: org.xml.sax.SAXParseException => error(e.getMessage) //@todo better handling of ``empty'' XML files 
    }
  
  def diff[M <: MetaData, T >: Node](eqsys: E[M, T], src: NodeSeq, tgt: NodeSeq) = new Differ(eqsys).diff(new NodeBuffer &+ (src map trimProper), new NodeBuffer &+ (tgt map trimProper))
}

/** <i>sdiff</i>: Semantics-Based XML Differencing Algorithm.
 * 
 *  <p>For further information please refer to:<br/> 
 *  Normen M&#xFC;ller, Change Management on Semi-Structured Documents, PhD thesis<br/>
 *  School of Engineering & Science, Jacobs University Bremen, Campus Ring 1, 28759 Bremen, Germany</p>
 * 
 *  @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 *  @version 0.0.3
 * 
 *  @thanks Serge Autexier and Marc Wagner for their initial discussions on the general differencing algorithm.
 */
private [diff] class Differ[M <: MetaData, T >: Node](eqsys: scaup.eq.EquivalenceSystem[M, T]) {
  import eqsys._
  import scala.xml.NodeSeq._
  
  def diff(src: NodeSeq, tgt: NodeSeq): List[Command] = diff(src, tgt, Root.childAt(1),Nil).reverse

  private def diff(src: NodeSeq, tgt: NodeSeq, path: LocationPath, cs: List[Command]): List[Command] = (src,tgt) match {
    case (Empty, Empty)                                   => cs        
    case (Seq(hs@Text(_), ts@_*), Empty)                  => diff(ts, Empty, path++, remove(path.parent.text, elem(hs)) :: cs)
    case (Seq(hs@Text(l), ts@_*), Seq(ht@Text(r), tt@_*)) => if(l != r) diff(ts, tt, path++, update(path.parent.text, elem(ht)) :: cs) else cs    
//  @draft: diff(ts, tt, path++, patch(path.parent.text, delta(scaup.util.Differ.diff(hs.text, ht.text))) :: cs)
    case (Empty, Seq(ht, tt@_*))                          => diff(Empty, tt, path, append(path.parent, elem(ht)) :: cs)      
    case (Seq(hs, ts@_*), Empty)                          => diff(ts, Empty, path++, remove(path, elem(hs)) :: cs)  	      
    case (Seq(hs, ts@_*), _)                              => ((slit: Slit[Node]) =>
      diff(ts, slit.after, path++, slit.focus match {
        case Some(f) => diff(hs.child, f.child, path childAt 1, diff(hs, hs.attributes, f.attributes, path) ::: (slit.before match {
          case Some(es) => List(before(path, (es map elem):_*))
          case _        => Nil
        }) ::: cs)            
        case _ => (slit.before match {
          case Some(es) => List(before(path, (es map elem):_*))
          case _        => Nil
        }) ::: List(remove(path, elem(hs))) ::: cs
      })
    )(findEquivTo(hs, tgt))
      
    //@alternative for previous case
    //@todo test!
//    case (Seq(hs, ts@_*), _) =>
//      val insert = (x: Option[Seq[Node]]) => x match {
//        case Some(es) => before(path, (es map elem):_*) :: Nil
//        case None     => Nil
//      }
//      ((slit: Slit[Node]) =>
//        diff(ts, slit.after, if(ts isEmpty) path-- else path++, slit.focus match {
//          case Some(f) => diff(hs.child, f.child, if(hs.child isEmpty) path else path.childAt(1),
//                               diff(hs, hs.attributes, f.attributes, path) ::: insert(slit.before) ::: cs)            
//          case _       => insert(slit.before) ::: List(remove(path, elem(hs))) ::: cs
//        })
//      )(findEquivTo(hs, tgt))
  }

  private def diff(owner: Node, src: MetaData, tgt: MetaData, path: LocationPath) =
    ((for(a: MetaData <- tgt) yield
       src.elements find (_.key == a.key) match {
         case Some(k) if (k.value != a.value) => Some(update(path.attribute(a.qname(owner)), elem(Text(a.value.text))))
         case None                            => Some(append(path.attributes, attr(a.copy(Null))))
         case _                               => None
    }).toList filter (None !=) map (_.get)
    ) :::
    (for(a: MetaData <- src if(! (tgt.elements exists (_.key == a.key)))) yield remove(path.attribute(a.qname(owner)), attr(a.copy(Null)))).toList
}
